package com.dhcc.service;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dhcc.domain.Stock;
import com.dhcc.lock.DistributedLockClient;
import com.dhcc.lock.DistributedRedisLock;
import com.dhcc.mapper.StockMapper;

import ch.qos.logback.core.util.TimeUtil;

@Service
//@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
public class StockService {

	Logger logger = LoggerFactory.getLogger(StockService.class);

	@Autowired
	private StockMapper mapper;

	@Autowired
	private StringRedisTemplate redisTemplate;
	
	@Autowired
	private DistributedLockClient redisLock;

//	private Stock stock = new Stock();

	private ReentrantLock lock = new ReentrantLock();
	/**
	 * 使用synchronized关键字解决单机的超卖现象
	 */
//	public synchronized void deduck() {
//		this.stock.setStock(stock.getStock()-1);
//		logger.info("当前库存======"+this.stock.getStock());
//	}

	/**
	 * 使用reentrantLock实现单机锁
	 */
//	public void deduck() {
//		lock.lock();
//		try {
//			this.stock.setStock(stock.getStock()-1);
//			logger.info("当前库存======"+this.stock.getStock());
//		} finally {
//			// TODO: handle finally clause
//			lock.unlock();
//		}
//	}

	/**
	 * 不加synchronized关键字在多线程的情况下，50的用户，每次100次请求 所售卖的库存呢结果可能是：0-4950：
	 * 原因：如果在同一时刻，50个用户同时发请求，100个请求在同一时刻发送到数据库，此时都判断库存5000>0 所以都去及逆行update
	 * count=5000-1 所以执行49个用户继续发 4999-1 ... 如此便是4950
	 */
//	public synchronized void deduck() {
//		try {
//			// 1、查询库存
//			Stock stock = this.mapper.selectOne(new QueryWrapper<Stock>().eq("product_code", "1001"));
//			// 2、判断库存是否充足
//			if(stock != null && stock.getCount()>0) {
//				stock.setCount(stock.getCount() - 1);
//				// 3、更新库存
//				this.mapper.updateById(stock);
//			}
//		} finally {
//			// TODO: handle finally clause
//			
//		}
////		logger.info("当前库存======"+this.stock.getStock());
//	}

	/**
	 * 三种情况会使得JVM锁失效：
	 * 1、多例模式（如果synchronized和关键字加载方法上，所锁的对象就是具体的实例对象，而不是类本身。在多例模式下，每次都会创建新的对象，导致同一时刻不同对象的锁互不干扰。仍会出现超卖现象）
	 * 2、事务（假如使用@Transcation注解后，开始事务，Session1：begin->select:5000-> update
	 * 5000-1;此时再没提交事务之前又来了个请求，session2->select 5000->update 5000-1;->此时session1
	 * 进行commit操作 count=4999->session2也进行commit操作 count=4999;本来应该是减库存两件，事务导致只减了一件）
	 * 3、分布式的情况(类似于多例模式)，在不同的机器下，JVM的本地锁也是隔离的，app1->一个线程获取锁进行减库存不影响app2的一个线程去进行减库存。这样一来，两次请求实则减了一次库存。
	 */

	/**
	 * 一个SQL语句可以解决超卖现象，让更新和判断操作保证了原子性 在集群部署下可以解决 多例模式下也可以解决
	 */
//	public void deduck() {
//		// 集群测试 Spring默认的史丹利Signlton
//		try {
//			// 使得更新和判断保持原子性，如果不是原子性，需要使用JVM锁机制实现
//			// 1个sql搞定： update stock set count = count -1 where product_code = '1001' and count >= 1;
//			// 1、查询库存
//			this.mapper.updateStock(1,"1001");
//			System.out.println("余数还有===="+mapper.selectById(1).getCount());
//		} finally {
//			// TODO: handle finally clause
//		}
//	}

	// 测试通过
//	public void deduck() {
//		// 多例模式测试
//		// 使用@Scope(value = "prototype",proxyMode = ScopedProxyMode.TARGET_CLASS)
//		try {
//			// 使得更新和判断保持原子性，如果不是原子性，需要使用JVM锁机制实现
//			// 1个sql搞定： update stock set count = count -1 where product_code = '1001' and count > 1;
//			// 1、查询库存
//			this.mapper.updateStock(1,"1001");
//			System.out.println("余数还有===="+mapper.selectById(1).getCount());
//		} finally {
//			// TODO: handle finally clause
//		}
//	}

//	@Transactional
//	public void deduck() {
//		// 事务测试
//		// 使用注解 @Transactional
//		try {
//			// 使得更新和判断保持原子性，如果不是原子性，需要使用JVM锁机制实现
//			// 1个sql搞定： update stock set count = count -1 where product_code = '1001' and count >= 1;
//			// 1、查询库存
//			this.mapper.updateStock(1,"1001");
//			System.out.println("余数还有===="+mapper.selectById(1).getCount());
//		} finally {
//			// TODO: handle finally clause
//		}
//	}

	// 使用一个sql语句解决超卖现象小结：
	/**
	 * 更新时判断库存是否够用 解决：三个锁失效 问题： 1、所范围问题，表级锁和行级锁 2、同一个商品有多个库存记录 3、无法记录库存变化后的状态
	 */

	/**
	 * 使用redis做分布式锁解决超卖问题 1、单机模式下，使用JVM本地所机制解决（多例或者集群存在超卖现象） 2、redis的乐观锁机制 watch
	 * multi exec watch 检控一个或者多个key的值，如果在事务执行之前，exec指令执行之前 如果key的值变化 则取消事务的执行，否则正常执行
	 * multi 开启事务执行，保证了命令的原子性 exec 提交事务 3、redis做分布式锁
	 */
//	public void deduck() {
//		// 存在超卖现象
//		try {
//			// 1、查询库存
//			String stock = redisTemplate.opsForValue().get("stock");
//			// 2、判断库存是否充足
//			if(stock != null && stock.length() != 0) {
//				Integer stcount = Integer.valueOf(stock);
//				if(stcount > 0) {
//					// 3、更新库存
//					this.redisTemplate.opsForValue().set("stock", String.valueOf(--stcount));
//					
//				}
//			}
//		} finally {
//			// TODO: handle finally clause
//			
//		}
////		logger.info("当前库存======"+this.stock.getStock());
//	}

	/**
	 * 乐观锁可以解决炒卖问题，但是性能锁好大
	 */
//	public void deduck() {
//		// 乐观锁可以解决超卖问题，但是性能损耗太大
//		this.redisTemplate.execute(new SessionCallback() {
//
//			@Override
//			public Object execute(RedisOperations operations) throws DataAccessException {
//					// 1、开启事务监听 watch
//					operations.watch("stock");
//					// 2、查询库存
//					String stock = redisTemplate.opsForValue().get("stock");
//					try {
//					// 3、判断库存是否充足
//					if(stock != null && stock.length() != 0) {
//						Integer stcount = Integer.valueOf(stock);
//						if(stcount > 0) {
//							// 4、开启事务
//							operations.multi();
//							// 5、更新库存
//							operations.opsForValue().set("stock", String.valueOf(--stcount));
//							// 6、提交事务
//							List exec = operations.exec();
//							// 7、判断事务的返回结果集如果结果为空，则重试
//							if(exec == null || exec.size() == 0) {
//								Thread.sleep(100);
//								deduck();
//							}
//							return exec;
//						}
//					}
//				} catch (InterruptedException e) {
//					// TODO Auto-generated catch block
//					e.printStackTrace();
//				} finally {
//				}
//				return null;
//			}
//		});
//		
//	}

	/**
	 * 使用redis 分布式锁解决超卖现象 分布式锁：跨进程、跨服务、跨服务器 场景：超卖现象（NoSQL）、缓存击穿
	 * 缓存击穿：一个热点的key失效，导致大量的请求直达数据库，导致服务宕机。
	 * 
	 * @throws InterruptedException
	 */
    /**
     * redis分布式锁
     */
//    public void deduck(){
//        // 先拿到分布式锁
//        String uuid= UUID.randomUUID().toString();
//        while(!redisTemplate.opsForValue().setIfAbsent("lock", uuid,10,TimeUnit.SECONDS)){
//            // 线程随眠50毫秒
//            try {
//                Thread.sleep(50);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
//        // 如果上锁成功就扣减库存
//        try{
//            // 获取库存并-1
//            String stock = redisTemplate.opsForValue().get("stock");
//            if(!StringUtils.isEmpty(stock)){
//                Integer count = Integer.valueOf(stock);
//                if(count > 0){
//                    redisTemplate.opsForValue().set("stock",String.valueOf(count-1));
//                }
//            }
//        }finally {
//            // 原子性解锁
//            String script  = "if redis.call('get',KEYS[1]) == ARGV[1] " +
//                        " then " +
//                        " return redis.call('del',KEYS[1])" +
//                        " else " +
//                        " return 0" +
//                        " end";
//            Boolean lock = redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock"), uuid);
//            }
//        }
	
	/**
	 * 可重入锁枷锁流程：ReentrantLock.lock() --》 NonfairSync.lock()--》AQS.acquire（1）--》NonfairSync.tryAcquire（1）--》Sync.nonfairTran
	 * 	1、CAS获取锁，如果所没有没占用（state==0），则枷锁成功并记录当前线程是有锁线程（两次）
	 *  2、如果state不是0,说明锁被站占用了，则判断当前线程是否是有🔒线程，如果是则state+1（重入操作）
	 *  3、否则枷锁失败，入队等待
	 *  
	 * 可重入锁解锁流程
	 * 	1、判断当前线程是否是有锁线程，不是则抛出异常
	 * 	2、对state的值减1之后，判断state是否等于0，如果是0，则unlock操作
	 *  3、否则返回false
	 * 
	 * 参考ReentrantLock，利用hash+lua脚本
	 * 枷锁:
	 * 	1、判断锁是否存在exists，则直接获取锁 hset key filed value
	 * 	2、如果锁存在则判断是否是自己的锁，hexists 如果是则重入 hincrby key filed increment
	 *  3、否则重试，循环、递归
	 *  
	 *  枷锁lua脚本
	 *  if redis.call('exists','lock') == 0 then redis.call('hset','lock',uuid,1) redis.call('expire','lock',30) return 1 elseif redis.call('hexists','lock',uuid) == 1 then redis.call('hincrby','lock',uuid,1) redis.call('expire','lock',30) return 1 else return 0 end
	 
	 	优化：
	 		if redis.call('exists','lock') == 0 or redis.call('hexists','lock',uuid) == 1 then redis.call('hincrby','lock',uuid,1) redis.call('expire','lock',30) return 1 else return 0 end
	 		eval "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then redis.call('hincrby',KEYS[1],ARGV[1],1) redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end" 1 lock uuid 30
	 	
	 	解锁lua脚本
	 	if redis.call('hincrby','lock',uuid,-1) == 0 then return redis.call('hdel','lock',uuid) elseif redis.call('hexists','lock',uuid) == 0 then return nil else return 0 end 
	 	eval "if redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 then return redis.call('hdel',KEYS[1],ARGV[1]) elseif redis.call('hexists',KEYS[1],ARGV[1]) == 0 then return nil else return 0 end" 1 lock uuid
	 */
//	public void deduck(){
//        // 先拿到分布式锁
//        String uuid= UUID.randomUUID().toString();
//        while(!redisTemplate.opsForValue().setIfAbsent("lock", uuid,10,TimeUnit.SECONDS)){
//            // 线程随眠50毫秒
//            try {
//                Thread.sleep(50);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        }
//        // 如果上锁成功就扣减库存
//        try{
//            // 获取库存并-1
//            String stock = redisTemplate.opsForValue().get("stock");
//            if(!StringUtils.isEmpty(stock)){
//                Integer count = Integer.valueOf(stock);
//                if(count > 0){
//                    redisTemplate.opsForValue().set("stock",String.valueOf(count-1));
//                }
//            }
//        }finally {
//            // 原子性解锁
//            String script  = "if redis.call('get',KEYS[1]) == ARGV[1] " +
//                        " then " +
//                        " return redis.call('del',KEYS[1])" +
//                        " else " +
//                        " return 0" +
//                        " end";
//            Boolean lock = redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList("lock"), uuid);
//            }
//        }
	
	public void deduck(){
		DistributedRedisLock redisLock2 = redisLock.getRedisLock("lock");
		redisLock2.lock();
        // 如果上锁成功就扣减库存
        try{
            // 获取库存并-1
            String stock = redisTemplate.opsForValue().get("stock");
            if(!StringUtils.isEmpty(stock)){
                Integer count = Integer.valueOf(stock);
                if(count > 0){
                    redisTemplate.opsForValue().set("stock",String.valueOf(count-1));
                }
            }
        }finally {
            redisLock2.unlock();
        }
	}
}
